package com.itealabs.shao.core;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

/**
 * 
 * @author itea
 * 2009-12-18
 * $Id: JarModuleClassLoader.java 71 2009-12-30 14:42:08Z iteahere $
 */
public class JarModuleClassLoader extends ClassLoader implements Module{

	protected File jar;
	
	protected Set<String> jarEntriesPath = new HashSet<String>();
	
	protected String main_class;
	
	public JarModuleClassLoader(File jar) throws IOException {
		super();
		if(jar == null) throw new NullPointerException("Argument jar cannot be null.");
		link(this.jar = jar.getCanonicalFile());
	}
	
	public JarModuleClassLoader(File jar, ClassLoader parent) throws IOException {
		super(parent);
		if(jar == null) throw new NullPointerException("Argument jar cannot be null.");
		link(this.jar = jar.getCanonicalFile());
	}
	
	@Override
	public File getJar() {
		return new File(jar.getAbsolutePath());
	}

	protected void link(File jar) throws IOException {
		
		JarFile jarFile = null;
		try {
			jarFile = new JarFile(jar);
			
			// load entries path from jar into jarEntriesPath
			Enumeration<JarEntry> enu = jarFile.entries();
			while(enu.hasMoreElements()) {
				JarEntry e = enu.nextElement();
				if(!e.isDirectory())
					jarEntriesPath.add(e.getName());
			}
			
			// get Main-Class
			Manifest mf = jarFile.getManifest();
			if(mf != null) {
				String mc = mf.getMainAttributes().getValue("Main-Class");
				if(jarEntriesPath.contains(this.className2resourceName(mc)))
					main_class = mc;
			}
		} finally {
			if(jarFile != null) jarFile.close();
		}
	}
	
	@Override
	public void unlink() {
		jarEntriesPath.clear();
	}

	protected static boolean isImplemented(Class<?> clz, Class<?> itf) {
		if(clz == null) return false;
		for(Class<?> c : clz.getInterfaces())
			if(c.equals(itf))
				return true;
		return isImplemented(clz.getSuperclass(), itf);
	}
	
	@Override
	protected synchronized Class<?> loadClass(String name, boolean resolve)
			throws ClassNotFoundException {
		if(!containsClass(name))
			return super.loadClass(name,resolve);
		
		Class<?> c = findLoadedClass(name);
		if (c == null)
		    c = findClass(name);
		
		if (resolve)
		    resolveClass(c);
		
		return c;
	}

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		String rn = className2resourceName(name);
		URL url = findResource(rn);
		if(url == null) throw new ClassNotFoundException(name);
		
		InputStream in = null;
		try {
			in = url.openStream();
			byte[] b = new byte[in.available()];
			in.read(b);
			return defineClass(name, b, 0, b.length);
		} catch (IOException e) {
			throw new ClassNotFoundException(name);
		} finally {
			if(in != null)
				try {
					in.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
		}
	}

	@Override
	protected URL findResource(String name) {
		if(!jarEntriesPath.contains(name)) return null;
		try {
			return new URL("jar:file:" + jar.getAbsolutePath() + "!/" + name);
		} catch (MalformedURLException e) {
			return null;
		}
	}

	@Override
	protected Enumeration<URL> findResources(String name) throws IOException {
		if(!jarEntriesPath.contains(name)) return null;
		try {
			final URL u = new URL("jar:file:" + jar.getAbsolutePath() + "!/" + name);
			return new Enumeration<URL>() {

				private URL us = u;
				@Override
				public boolean hasMoreElements() {
					return us != null;
				}

				@Override
				public URL nextElement() {
					URL x = us;
					us = null;
					return x;
				}
				
			};
		} catch (MalformedURLException e) {
			return super.findResources(name);
		}
	}
	
	protected String className2resourceName(String n) {
		return n == null? null : n.replace(".", "/").concat(".class");
	}

	@Override
	public String toString() {
		return "JarModuleClassLoader [jar=" + jar + "]";
	}

	@Override
	public boolean containsResource(String name) {
		//checkStatus(Status.LINKED);
		return jarEntriesPath.contains(name);
	}

	@Override
	public boolean containsClass(String name) {
		//checkStatus(Status.LINKED);
		return jarEntriesPath.contains(className2resourceName(name));
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((jar == null) ? 0 : jar.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		JarModuleClassLoader other = (JarModuleClassLoader) obj;
		if (jar == null) {
			if (other.jar != null)
				return false;
		} else if (!jar.equals(other.jar))
			return false;
		return true;
	}

	@Override
	public boolean isApplication() {
		return main_class != null;
	}
	
	@Override
	public void runApplication(String... args) throws Exception {
		if(!isApplication())
			throw new UnsupportedOperationException("This module is not an application.");
		
		Class<?> mc = loadClass(main_class);
		Method method = mc.getMethod("main");
		method.invoke(null, (Object[])args);
	}
	
	@Override
	public ClassLoader getClassLoader() {
		return this;
	}
}
